/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.util;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * TODO Add UserProperties Overview JavaDoc.
 * <p>
 * 
 * @author Mark A. Kobold &lt;mkobold at isqlviewer dot com&gt;
 * @version 1.0
 */
public class UserProperties extends Properties {

    private static final long serialVersionUID = 6738010317137968113L;

    /**
     * Creates new properties object with given defaults.
     * <p>
     * 
     * @see Properties#Properties(java.util.Properties)
     * @param defaults default properties object to use.
     */
    public UserProperties(Properties defaults) {

        super(defaults);
    }

    /**
     * Creates new properties object with no defaults.
     * <p>
     * 
     * @see Properties#Properties()
     */
    public UserProperties() {

        super();
    }

    /**
     * Removes a property from current mapping.
     * <p>
     * 
     * @param property property name to remove.
     * @return property as a string, null if the property does not exist.
     */
    public String removeProperty(String property) {

        try {
            return super.remove(property.toLowerCase().trim()).toString();
        } catch (NullPointerException npe) {
            return null;
        }
    }

    /**
     * Renames an existing property to a new name.
     * <p>
     * 
     * @param oldName the old property name to change.
     * @param newName property name to change the oldName to.
     */
    public synchronized final void renameProperty(String oldName, String newName) {

        if (containsKey(oldName)) {
            Object value = getObjectProperty(oldName);
            if (value == null) {
                throw new NullPointerException("Cannot rename non-existent property.");
            }
            remove(oldName);
            put(newName, value);
            rehash();
        } else if (!containsKey(newName)) {
            put(newName, "");
        }
    }

    /**
     * Takes the a list of keys then iterators through the list and validates that all keys are contained as keys with
     * the UserProperties.
     * 
     * @param keySet the keyset to validate againist
     * @return false if the key is not contained
     */
    public boolean containsAllKeys(Collection keySet) {

        if (keySet == null || keySet.isEmpty()) {
            return isEmpty();
        }

        synchronized (keySet) {
            Iterator itr = keySet.iterator();
            while (itr.hasNext()) {
                if (!containsKey(itr.next())) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Removes all keys from map.
     * <p>
     * Takes the keys from map and removes them from this object. In terms of set theory the properties are removed by
     * the intersection of the given map and this object.
     * 
     * @param map map that contains the keys to removes.
     */
    public synchronized final void removeAll(Map map) {

        Iterator i = map.keySet().iterator();
        synchronized (this) {
            while (i.hasNext()) {
                remove(i.next());
            }
        }
    }

    /**
     * Associates a string representing the specified boolean value.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose boolean form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getBoolean(String, boolean)
     */
    public final void putBoolean(String key, boolean value) {

        setObjectProperty(key, Boolean.toString(value));
    }

    /**
     * Associates a string representing the specified int value.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose int form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getInt(String, int)
     */
    public final void putInt(String key, int value) {

        setObjectProperty(key, Integer.toString(value));
    }

    /**
     * Associates a string representing the specified double value.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose double form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getDouble(String, double)
     */

    public final void putDouble(String key, double value) {

        setObjectProperty(key, Double.toString(value));
    }

    /**
     * Associates a string representing the specified float value.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose float form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getFloat(String, float)
     */
    public final void putFloat(String key, float value) {

        setObjectProperty(key, Float.toString(value));
    }

    /**
     * Associates a string representing the specified long value.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose long form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getLong(String, long)
     */
    public final void putLong(String key, long value) {

        setObjectProperty(key, Long.toString(value));
    }

    /**
     * Sets the short property with the given key name.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose long form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getShort(String, short)
     */

    public final void putShort(String key, short value) {

        setObjectProperty(key, Short.toString(value));
    }

    /**
     * Sets the byte property with the given key name.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose byte form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #getShort(String, short)
     */
    public final void putByte(String key, byte value) {

        setObjectProperty(key, Byte.toString(value));
    }

    /**
     * Associates a string representing the specified String value.
     * <p>
     * 
     * @param key key with which the string form of value is to be associated.
     * @param value value whose String form is to be associated with key.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #get(String, String)
     */
    public final void put(String key, String value) {

        setObjectProperty(key, value);
    }

    /**
     * Returns the boolean value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a double.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt> or
     *        the associated value cannot be interpreted as a boolean.
     * @return the boolean value represented by the string associated with <tt>key</tt> in this object, or
     *         <tt>def</tt> if the associated value does not exist or cannot be interpreted as a boolean.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putBoolean(String, boolean)
     * @see #get(String,String)
     */
    public final boolean getBoolean(String key, boolean def) {

        Object value = getObjectProperty(key);
        if (value != null) {
            if (value instanceof Boolean) {
                return ((Boolean) value).booleanValue();
            } else if (value instanceof String) {
                return Boolean.valueOf((String) value).booleanValue();
            } else {
                return def;
            }
        }
        return def;
    }

    /**
     * Returns the boolean value of the string associated with the specified key.
     * <p>
     * This returns false as the default value.
     * 
     * @param key key whose associated value is to be returned as a boolean.
     * @return the boolean value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putBoolean(String, boolean)
     * @see #get(String,String)
     */
    public final boolean getBoolean(String key) {

        return getBoolean(key, false);
    }

    /**
     * Returns the int value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a int.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt> or
     *        the associated value cannot be interpreted as an int.
     * @return the boolean value represented by the string associated with <tt>key</tt> in this object, or
     *         <tt>def</tt> if the associated value does not exist or cannot be interpreted as an int.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putInt(String, int)
     * @see #get(String,String)
     */

    public final int getInt(String key, int def) {

        Object value = getObjectProperty(key);
        if (value != null) {
            if (value instanceof Integer) {
                return ((Integer) value).intValue();
            } else if (value instanceof Long) {
                return ((Long) value).intValue();
            } else if (value instanceof String) {
                return Integer.decode((String) value).intValue();
            } else {
                return def;
            }
        }
        return def;
    }

    /**
     * Returns the int value of the string associated with the specified key.
     * <p>
     * This returns Integer.MIN_VALUE as the default value.
     * 
     * @param key key whose associated value is to be returned as a double.
     * @return the int value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putInt(String, int)
     * @see #get(String,String)
     */
    public final int getInt(String key) {

        return getInt(key, Integer.MIN_VALUE);
    }

    /**
     * Returns the double value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a double.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt> or
     *        the associated value cannot be interpreted as a float.
     * @return the double value represented by the string associated with <tt>key</tt> in this object, or <tt>def</tt>
     *         if the associated value does not exist or cannot be interpreted as a double.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putDouble(String, double)
     * @see #get(String,String)
     */
    public final double getDouble(String key, double def) {

        Object value = getObjectProperty(key);
        if (value != null) {
            if (value instanceof Double) {
                return ((Double) value).doubleValue();
            } else if (value instanceof String) {
                return Double.parseDouble((String) value);
            } else {
                return def;
            }
        }
        return def;
    }

    /**
     * Returns the double value of the string associated with the specified key.
     * <p>
     * This will use the constant Double.NaN as the default value.
     * 
     * @param key key whose associated value is to be returned as a double.
     * @return the double value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putDouble(String,double)
     * @see #get(String,String)
     */
    public final double getDouble(String key) {

        return getDouble(key, Double.NaN);
    }

    /**
     * Returns the float value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a float.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt> or
     *        the associated value cannot be interpreted as a float.
     * @return the float value represented by the string associated with <tt>key</tt> in this object, or <tt>def</tt>
     *         if the associated value does not exist or cannot be interpreted as a float.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putFloat(String, float)
     * @see #get(String,String)
     */
    public final float getFloat(String key, float def) {

        Object value = getObjectProperty(key);
        if (value != null) {
            if (value instanceof Float) {
                return ((Float) value).floatValue();
            } else if (value instanceof Double) {
                return ((Double) value).floatValue();
            } else if (value instanceof String) {
                return Float.parseFloat((String) value);
            } else {
                return def;
            }
        }
        return def;
    }

    /**
     * Returns the float value of the string associated with the specified key.
     * <p>
     * This returns Float.NaN as the default value.
     * 
     * @param key key whose associated value is to be returned as a float.
     * @return the float value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putFloat(String, float)
     * @see #get(String,String)
     */
    public final float getFloat(String key) {

        return getFloat(key, Float.NaN);
    }

    /**
     * Returns the long value of the string associated with the specified key.
     * <p>
     * This returns Long.MIN_VALUE as the default value.
     * 
     * @param key key whose associated value is to be returned as a long.
     * @return the long value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putLong(String, long)
     * @see #get(String,String)
     */
    public final long getLong(String key) {

        return getLong(key, Long.MIN_VALUE);
    }

    /**
     * Returns the long value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a long.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt> or
     *        the associated value cannot be interpreted as a long.
     * @return the long value represented by the string associated with <tt>key</tt> in this object, or <tt>def</tt>
     *         if the associated value does not exist or cannot be interpreted as a long.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putLong(String, long)
     * @see #get(String,String)
     */
    public final long getLong(String key, long def) {

        Object value = getObjectProperty(key);
        if (value != null) {
            if (value instanceof Long) {
                return ((Long) value).longValue();
            } else if (value instanceof String) {
                return Long.decode((String) value).longValue();
            } else {
                return def;
            }
        }
        return def;
    }

    /**
     * Returns the short value of the string associated with the specified key.
     * <p>
     * This returns Short.MIN_VALUE as the default value.
     * 
     * @param key key whose associated value is to be returned as a long.
     * @return the short value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putShort(String, short)
     * @see #get(String,String)
     */
    public final short getShort(String key) {

        return getShort(key, Short.MIN_VALUE);
    }

    /**
     * Returns the short value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a short.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt> or
     *        the associated value cannot be interpreted as a short.
     * @return the long value represented by the string associated with <tt>key</tt> in this object, or <tt>def</tt>
     *         if the associated value does not exist or cannot be interpreted as a short.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #putShort(String, short)
     * @see #get(String,String)
     */
    public final short getShort(String key, short def) {

        Object value = getObjectProperty(key);
        if (value != null) {
            if (value instanceof Short) {
                return ((Short) value).shortValue();
            } else if (value instanceof Byte) {
                return ((Byte) value).shortValue();
            } else if (value instanceof String) {
                return Short.decode((String) value).shortValue();
            } else {
                return def;
            }
        }
        return def;
    }

    /**
     * Returns the String value of the string associated with the specified key.
     * <p>
     * 
     * @param key key whose associated value is to be returned as a char.
     * @param def the value to be returned in the event that this object has no value associated with <tt>key</tt>.
     * @return the String value represented by the string associated with <tt>key</tt> in this object, or <tt>def</tt>
     *         if the associated value does not exist.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #put(String, String)
     * @see Properties#getProperty(java.lang.String, java.lang.String)
     */
    public final String get(String key, String def) {

        Object value = getObjectProperty(key.toLowerCase().trim());
        if (value != null) {
            return value.toString();
        }
        return def;
    }

    @Override
    public final boolean containsKey(Object key) {

        if (key == null) {
            return false;
        } else if (key instanceof String) {
            return super.containsKey(((String) key).toLowerCase().trim());
        }
        throw new IllegalArgumentException("Keys for this map must be of type String.");
    }

    /**
     * Returns the String value of the string associated with the specified key.
     * <p>
     * This returns <tt>null</tt> as the default value.
     * 
     * @param key key whose associated value is to be returned as a String.
     * @return the String value represented by the string associated with <tt>key</tt> in this object.
     * @throws NullPointerException if <tt>key</tt> is <tt>null</tt>.
     * @see #put(String, String)
     */
    public final String get(String key) {

        return get(key, null);
    }

    @Override
    public final Object put(Object key, Object value) {

        if (key instanceof String) {
            return setObjectProperty((String) key, value);
        }
        throw new IllegalArgumentException("Keys for this map must be of type String.");
    }

    @Override
    public final Object get(Object key) {

        if (key instanceof String) {
            return getObjectProperty((String) key);
        }
        throw new IllegalArgumentException("Keys for this map must be of type String.");
    }

    @Override
    public String getProperty(String key, String defaultValue) {

        return get(key, defaultValue);
    }

    @Override
    public String getProperty(String key) {

        return get(key, null);
    }

    protected Object getObjectProperty(String key) {

        checkKey(key);
        String normalized = key.toLowerCase().trim();
        Object object = super.get(normalized);
        if (object == null && defaults != null) {
            return defaults.get(normalized);
        }
        return object;
    }

    protected Object setObjectProperty(String key, Object value) {

        checkKey(key);
        Class clazz = value.getClass();
        if (isPrimative(clazz) || value instanceof String) {
            String newKey = key.toLowerCase().trim();
            return super.put(newKey, value);
        }
        throw new IllegalArgumentException("Invaild Object given only primatives and strings are allowed.");
    }

    private boolean isPrimative(Class clazz) {

        if (clazz.isPrimitive()) {
            return true;
        }
        return (clazz == Boolean.class || clazz == Integer.class || clazz == Long.class || clazz == Short.class
                || clazz == Byte.class || clazz == Float.class || clazz == Double.class);

    }

    // method for validating keys for this object.
    private static void checkKey(String key) {

        if (key == null) {
            throw new NullPointerException("Null Keys are not allowed.");
        }
    }
}
